﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.DirectX.Direct3D;
using SimulationCore.Net.DTO;
using SimulationCore.Net.Misc;
using StillDesign.PhysX;
using StillDesign;
using Microsoft.DirectX;
using MeshData=StillDesign.PhysX.MeshData;

namespace SimulationCore.Net
{
    public class SimulationProvider
    {
        private OutputStream physxOutputStream = new OutputStream();
        private Core core;
        private Scene scene;

        /// <summary>
        /// Keeps a list of actors to remove
        /// </summary>
        private List<StillDesign.PhysX.IDisposable> actorsToDisposeQueue = new List<StillDesign.PhysX.IDisposable>();

        /// <summary>
        /// Sets up PhysX
        /// </summary>
        public void Setup()
        {
            core = new Core(new CoreDescription
                                      {
                                          Flags = CoreCreationFlag.NoHardware
                                      },
                                physxOutputStream);

            core.SetParameter(PhysicsParameter.VisualizationScale, 1);
            core.SetParameter(PhysicsParameter.VisualizeClothMesh, true);
            core.SetParameter(PhysicsParameter.VisualizeClothValidBounds, true);
            core.SetParameter(PhysicsParameter.SkinWidth, 0.005f);


            scene = core.CreateScene(new SceneDescription
                                           {
                                               Gravity = new Vector3(0, -9.81f, 0)
                                           });

            scene.CreateMaterial(new MaterialDescription()
                                     {
                                         Restitution = 0.5f,
                                         StaticFriction = 0.2f,
                                         DynamicFriction = 0.2f
                                     });
        }

        public Matrix AddBox(Vector3 size, Matrix initialTransformation, string name)
        {
            var boxShapeDescription = new BoxShapeDescription(size);

            var actorDesc = new ActorDescription()
            {
                Name = name,
                BodyDescription = new BodyDescription(1.0f),
                GlobalPose = initialTransformation,
                Shapes = { boxShapeDescription }
            };

            var actor = scene.CreateActor(actorDesc);
            return actor.GlobalPose;
        }

        /// <summary>
        /// <remarks>Ommits metal cloths' cores</remarks>
        /// </summary>
        /// <param name="timeSpan"></param>
        /// <returns></returns>
        public IList<ActorState> GetActorsState(TimeSpan timeSpan)
        {
            var actorsStates = new List<ActorState>();
            //Take only actors with names
            var actors = scene.Actors.Where(x => !string.IsNullOrEmpty(x.Name));
            scene.Simulate(timeSpan.Milliseconds / 100.0f);
            scene.FetchResults(SimulationStatus.RigidBodyFinished, true);


            foreach (var actor in actors)
            {
                if (actor.Name.Contains(metalClothCoreSuffix))
                {
                    //Ommit metal cloths' cores, since they do not need to be drawn
                    continue;
                }
                actorsStates.Add(new RidigActorState
                                     {
                                         Transformation = actor.GlobalPose,
                                         Name = actor.Name
                                     });
            }

            foreach (var cloth in scene.Cloths)
            {
                actorsStates.Add(new ClothState
                                     {
                                         //Remove suffix from cloth name
                                         Name = cloth.Name.Replace(metalClothSuffix, string.Empty),
                                         Vertices = cloth.MeshData.PositionsStream.GetData<Vector3>(),
                                         Normals = cloth.MeshData.NormalsStream.GetData<Vector3>(),
                                         //Todo: Pay attention to this Cast - originally was <Vector3>
                                         indices = cloth.MeshData.IndicesStream.GetData<int>().Select(x => (short) x).ToArray()
                                     });
            }

            RemovePendingActors();
            return actorsStates;
        }

        public void AddGroundPlane()
        {
            var groundPlane = new PlaneShapeDescription();
            scene.CreateActor(new ActorDescription
                                  {
                                      Shapes = { groundPlane }
                                  });
        }

        /// <summary>
        /// Creates regular, spherical joint between two Actors given by name
        /// </summary>
        /// <param name="actor1"></param>
        /// <param name="actor2"></param>
        /// <param name="joinPoint"></param>
        public void JoinActors(string actor1, string actor2, Vector3 joinPoint, string name)
        {
            var sceneActor1 = scene.Actors.First(x => x.Name == actor1);
            var sceneActor2 = scene.Actors.First(x => x.Name == actor2);

            var jointDesc = new SphericalJointDescription()
                                {
                                    Actor1 = sceneActor1,
                                    Actor2 = sceneActor2,
                                    Name = name
                                };

            jointDesc.SetGlobalAnchor(joinPoint);
            scene.CreateJoint(jointDesc);
        }

        public void CreateMetalCloth(string name, Matrix initialTransformation, float impulseThreshold, float penetrationDepth, Vector3[] vertices, int[] indices)
        {
            #region Cloth Mesh Description
            var clothMeshDescription = new ClothMeshDescription();
            clothMeshDescription.AllocateVertices<Vector3>(vertices.Length);
            clothMeshDescription.AllocateTriangles<int>(indices.Length / 3);

            clothMeshDescription.VertexCount = vertices.Length;
            clothMeshDescription.TriangleCount = indices.Length / 3;

            clothMeshDescription.VerticesStream.SetData(vertices);
            clothMeshDescription.TriangleStream.SetData(indices); 
            #endregion

            #region Cooking Mesh
            var memoryStream = new MemoryStream(200);
            Cooking.InitializeCooking();
            Cooking.CookClothMesh(clothMeshDescription, memoryStream);
            Cooking.CloseCooking();
            // Need to reset the position of the stream to the beginning
            memoryStream.Position = 0; 
            #endregion

            var clothMesh = scene.Core.CreateClothMesh(memoryStream);

            #region Cloth Description
            var clothDescription = new ClothDescription
                                       {
                                           ClothMesh = clothMesh,
                                           Name = name + metalClothSuffix,
                                           Flags = ClothFlag.BendingOrthogonal,
                                           Friction = 0.2f,
                                           Thickness = 0.1f
                                       };
            clothDescription.MeshData.AllocatePositions<Vector3>(vertices.Length);
            clothDescription.MeshData.AllocateIndices<int>(indices.Length);
            clothDescription.MeshData.AllocateNormals<Vector3>(vertices.Length);

            clothDescription.MeshData.MaximumVertices = vertices.Length;
            clothDescription.MeshData.MaximumIndices = indices.Length;

            clothDescription.MeshData.NumberOfVertices = vertices.Length;
            clothDescription.MeshData.NumberOfIndices = indices.Length; 
            #endregion

            var cloth = scene.CreateCloth(clothDescription);

            var actorDesc = new ActorDescription()
            {
                Name = name + metalClothCoreSuffix,
                BodyDescription = new BodyDescription(1.0f),
                GlobalPose = initialTransformation,
                Shapes = {new BoxShapeDescription() }
            };

            var actor = scene.CreateActor(actorDesc);
            cloth.AttachToCore(actor, impulseThreshold, penetrationDepth);
        }

        public Matrix CreateMesh(string name, Vector3[] vertices, short[] indices, Matrix initialTransformation)
        {
            var indices32 = indices.Select(x => (int)x).ToArray();

            var meshDescription = new ConvexMeshDescription
                                      {
                                          PointCount = vertices.Length,
                                          TriangleCount = indices.Count() / 3,
                                      };
            meshDescription.Flags |= ConvexFlag.ComputeConvex;
            meshDescription.AllocatePoints<Vector3>(vertices.Count());
            meshDescription.AllocateTriangles<int>(indices.Count() / 3);
            foreach (var vertex in vertices)
            {
                vertex.Scale(10.0f);
                meshDescription.PointsStream.Write(vertex);
            }
            meshDescription.TriangleStream.SetData(indices32);

            var stream = new MemoryStream();

            Cooking.InitializeCooking(new ConsoleOutputStream());
            Cooking.CookConvexMesh(meshDescription, stream);
            Cooking.CloseCooking();

            stream.Position = 0;
            var convexMesh = scene.Core.CreateConvexMesh(stream);

            var convexShapeDesc = new ConvexShapeDescription(convexMesh);

            var actorDesc = new ActorDescription()
            {
                BodyDescription = new BodyDescription(10.0f),
                GlobalPose = initialTransformation,
                Name = name
            };
            actorDesc.Shapes.Add(convexShapeDesc);

            var actor = scene.CreateActor(actorDesc);

            return actor.GlobalPose;
        }

        #region Actors removal
        /// <summary>
        /// Request removal of Cloth actor from a scene. Actual removal is done when next step of simulation is done.
        /// </summary>
        /// <param name="name"></param>
        /// <exception cref="ArgumentOutOfRangeException">When actor is not found on a scene</exception>
        public void RemoveRidigActor(string name)
        {            
            var actorsToRemove = scene.Actors.Where(x => name == x.Name);
            if (null == actorsToRemove)
            {
                throw new ArgumentOutOfRangeException("name", "There are no actors with specified name on a scene");
            }
            if (1 == actorsToRemove.Count())
            {
                actorsToDisposeQueue.Add(actorsToRemove.ElementAt(0));
            }
        }

        /// <summary>
        /// Request removal of Cloth actor from a scene. Actual removal is done when next step of simulation is done.
        /// </summary>
        /// <param name="name"></param>
        /// <exception cref="ArgumentOutOfRangeException">When actor is not found on a scene</exception>
        public void RemoveClothActor(string name)
        {
            var clothName = string.Format("{0}{1}", name, metalClothSuffix);
            var actorsToRemove = scene.Cloths.Where(x => clothName == x.Name);
            if (null == actorsToRemove)
            {
                throw new ArgumentOutOfRangeException("name", "There are no actors with specified name on a scene");
            }
            if (1 == actorsToRemove.Count())
            {
                actorsToDisposeQueue.Add(actorsToRemove.ElementAt(0));
            }
        }

        /// <summary>
        /// Commits Remove action of Actors requested by Remove*Actor(string) methods.
        /// This must be done after scene is finished simulating
        /// </summary>
        private void RemovePendingActors()
        {
            foreach (var actor in actorsToDisposeQueue)
            {
                actor.Dispose();
            }
            actorsToDisposeQueue.Clear();
        } 
        #endregion


        private readonly string metalClothSuffix = "_cloth";
        private readonly string metalClothCoreSuffix = "_core";

        /// <summary>
        /// Remove later!
        /// 
        /// Generates simple VertexPlane
        /// </summary>
        public class VertexGrid
        {
            private VertexGrid(Vector3[] points, int[] indices)
            {
                this.Points = points;
                this.Indices = indices;
            }

            public static VertexGrid CreateGrid(int rows, int columns)
            {
                int numVertsX = rows + 1;
                int numVertsZ = columns + 1;

                VertexGrid grid = new VertexGrid(new Vector3[numVertsX * numVertsZ], new int[rows * columns * 2 * 3]);

                {
                    for (int r = 0; r < numVertsX; r++)
                    {
                        for (int c = 0; c < numVertsZ; c++)
                        {
                            grid.Points[r * numVertsZ + c] = new Vector3(r, 0, c);
                        }
                    }
                }

                {
                    int count = 0;
                    int vIndex = 0;
                    for (int z = 0; z < columns; z++)
                    {
                        for (int x = 0; x < rows; x++)
                        {
                            // first triangle
                            grid.Indices[count++] = vIndex + numVertsX;
                            grid.Indices[count++] = vIndex + 1;
                            grid.Indices[count++] = vIndex;

                            // second triangle
                            grid.Indices[count++] = vIndex + numVertsX + 1;
                            grid.Indices[count++] = vIndex + 1;
                            grid.Indices[count++] = vIndex + numVertsX;

                            vIndex++;
                        }
                        vIndex++;
                    }
                }

                return grid;
            }

            public Vector3[] Points
            {
                get;
                private set;
            }
            public int[] Indices
            {
                get;
                private set;
            }
        }


    }

    internal static class SpoolHelper
    {
        public delegate TOutput StreamConverter<TStreamBase, TOutput>(TStreamBase input);

        static internal TOutput[] ToArray<TStreamBase, TOutput>(this GraphicsStream graphicsStream, StreamConverter<TStreamBase, TOutput> convert, int numberOfElements)
        {
            Array streamArrayRaw = graphicsStream.Read(typeof(TStreamBase), numberOfElements);

            var streamArrayOut = new TOutput[streamArrayRaw.Length];
            {
                int index = 0;
                foreach (TStreamBase vertex in streamArrayRaw)
                {
                    streamArrayOut[index++] = convert(vertex);
                }
            }

            return streamArrayOut;
        }
    }
}
